<!DOCTYPE html>
<html lang="en">
    <head>
        <title>three.js webgl - postprocessing - unreal bloom selective</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
        <link type="text/css" rel="stylesheet" href="threejs.css">
    </head>
    <body>

        <div id="info">
            <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> Click on an object to toggle bloom<br>By pieper based on example from <a href="http://github.com/Temdog007" target="_blank" rel="noopener">Temdog007</a>
            <p> Please wait while content is generated and loaded... (be sure Slicer's exec API is enabled)</p>
        </div>

        <script type="x-shader/x-vertex" id="vertexshader">

            varying vec2 vUv;

            void main() {

                vUv = uv;

                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

            }

        </script>

        <script type="x-shader/x-fragment" id="fragmentshader">

            uniform sampler2D baseTexture;
            uniform sampler2D bloomTexture;

            varying vec2 vUv;

            void main() {

                gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );

            }

        </script>

        <!-- Import maps polyfill -->
        <!-- Remove this when import maps will be widely supported -->
        <!--
        <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

        <script type="importmap">
            {
                "imports": {
                    "three": "../build/three.module.js",
                    "three/addons/": "./jsm/"
                }
            }
        </script>
        -->
        <script type="importmap">
          {
            "imports": {
              "three": "https://unpkg.com/three@0.152.2/build/three.module.js",
              "three/addons/": "https://unpkg.com/three@0.152.2/examples/jsm/"
            }
          }
        </script>

        <script type="module">

            import * as THREE from 'three';

            import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
            import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

            import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
            import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
            import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
            import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
            import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

            THREE.ColorManagement.enabled = false; // TODO: Confirm correct color management.

            const ENTIRE_SCENE = 0, BLOOM_SCENE = 1;

            const bloomLayer = new THREE.Layers();
            bloomLayer.set( BLOOM_SCENE );

            const params = {
                exposure: 1,
                bloomStrength: 5,
                bloomThreshold: 0,
                bloomRadius: 0,
                scene: 'Scene with Glow'
            };

            const darkMaterial = new THREE.MeshBasicMaterial( { color: 'black' } );
            const materials = {};

            const renderer = new THREE.WebGLRenderer( { antialias: true } );
            renderer.setPixelRatio( window.devicePixelRatio );
            renderer.setSize( window.innerWidth, window.innerHeight );
            renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
            renderer.toneMapping = THREE.ReinhardToneMapping;
            document.body.appendChild( renderer.domElement );

            const scene = new THREE.Scene();

            const camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1500 );
            camera.position.set( 0, 0, 500 );
            camera.lookAt( 0, 0, 0 );

            const controls = new OrbitControls( camera, renderer.domElement );
            controls.maxPolarAngle = Math.PI * 0.5;
            controls.minDistance = 1;
            controls.maxDistance = 1000;
            controls.addEventListener( 'change', render );

            scene.add( new THREE.AmbientLight( 0x404040 ) );

            const renderScene = new RenderPass( scene, camera );

            const bloomPass = new UnrealBloomPass( new THREE.Vector2( window.innerWidth, window.innerHeight ), 1.5, 0.4, 0.85 );
            bloomPass.threshold = params.bloomThreshold;
            bloomPass.strength = params.bloomStrength;
            bloomPass.radius = params.bloomRadius;

            const bloomComposer = new EffectComposer( renderer );
            bloomComposer.renderToScreen = false;
            bloomComposer.addPass( renderScene );
            bloomComposer.addPass( bloomPass );

            const finalPass = new ShaderPass(
                new THREE.ShaderMaterial( {
                    uniforms: {
                        baseTexture: { value: null },
                        bloomTexture: { value: bloomComposer.renderTarget2.texture }
                    },
                    vertexShader: document.getElementById( 'vertexshader' ).textContent,
                    fragmentShader: document.getElementById( 'fragmentshader' ).textContent,
                    defines: {}
                } ), 'baseTexture'
            );
            finalPass.needsSwap = true;

            const finalComposer = new EffectComposer( renderer );
            finalComposer.addPass( renderScene );
            finalComposer.addPass( finalPass );

            const raycaster = new THREE.Raycaster();

            const mouse = new THREE.Vector2();

            window.addEventListener( 'pointerdown', onPointerDown );

            const gui = new GUI();

            gui.add( params, 'scene', [ 'Scene with Glow', 'Glow only', 'Scene only' ] ).onChange( function ( value ) {

                switch ( value )     {

                    case 'Scene with Glow':
                        bloomComposer.renderToScreen = false;
                        break;
                    case 'Glow only':
                        bloomComposer.renderToScreen = true;
                        break;
                    case 'Scene only':
                        // nothing to do
                        break;

                }

                render();

            } );

            const folder = gui.addFolder( 'Bloom Parameters' );

            folder.add( params, 'exposure', 0.1, 2 ).onChange( function ( value ) {

                renderer.toneMappingExposure = Math.pow( value, 4.0 );
                render();

            } );

            folder.add( params, 'bloomThreshold', 0.0, 1.0 ).onChange( function ( value ) {

                bloomPass.threshold = Number( value );
                render();

            } );

            folder.add( params, 'bloomStrength', 0.0, 10.0 ).onChange( function ( value ) {

                bloomPass.strength = Number( value );
                render();

            } );

            folder.add( params, 'bloomRadius', 0.0, 1.0 ).step( 0.01 ).onChange( function ( value ) {

                bloomPass.radius = Number( value );
                render();

            } );

            setupScene();

            function onPointerDown( event ) {

                mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
                mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

                raycaster.setFromCamera( mouse, camera );
                const intersects = raycaster.intersectObjects( scene.children, false );
                if ( intersects.length > 0 ) {

                    const object = intersects[ 0 ].object;
                    object.layers.toggle( BLOOM_SCENE );
                    render();

                }

            }

            window.onresize = function () {

                const width = window.innerWidth;
                const height = window.innerHeight;

                camera.aspect = width / height;
                camera.updateProjectionMatrix();

                renderer.setSize( width, height );

                bloomComposer.setSize( width, height );
                finalComposer.setSize( width, height );

                render();

            };

            function loadThreeScene() {

                scene.traverse( disposeMaterial );
                scene.children.length = 0;

                /* fiducials as spheres
                */
                const geometry = new THREE.IcosahedronGeometry( 10, 15 );
                const fiducialsXRH = new XMLHttpRequest();
                const fiducialsURL = "/slicer/fiducials";
                fiducialsXRH.open('GET', fiducialsURL, true);
                fiducialsXRH.responseType = 'json';
                fiducialsXRH.onload = function() {
                    let nodeID;
                    for (nodeID in this.response) {
                        let markupIndex;
                        for (markupIndex in this.response[nodeID].markups) {
                            const markup = this.response[nodeID].markups[markupIndex];
                            const color = new THREE.Color();
                            color.setHSL( Math.random(), 0.7, Math.random() * 0.2 + 0.05 );
                            const material = new THREE.MeshBasicMaterial( { color: color } );
                            const sphere = new THREE.Mesh( geometry, material );
                            sphere.position.x = markup.position[0];
                            sphere.position.y = markup.position[1];
                            sphere.position.z = markup.position[2];
                            sphere.rotateX(-1 * Math.PI/2.);
                            sphere.rotateZ(Math.PI);
                            scene.add( sphere );
                            if ( Math.random() < 0.25 ) sphere.layers.enable( BLOOM_SCENE );
                        }
                    }
                    render();
                };
                fiducialsXRH.send();

                /* graphics
                */
                const graphicsXRH = new XMLHttpRequest();
                const graphicsURL = "/slicer/threeDGraphics";
                graphicsXRH.open('GET', graphicsURL, true);
                graphicsXRH.responseType = 'json';

                graphicsXRH.onload = function() {
                    const loader = new GLTFLoader();
                    loader.parse(this.response,
                      ".",
                      function (gltf) {
                        window.gltf = gltf;
                        let object3D = gltf.scene.children[0];
                        for (let child in object3D.children) {
                            const color = new THREE.Color();
                            color.setHSL( Math.random(), 0.7, Math.random() * 0.2 + 0.05 );
                            const material = new THREE.MeshBasicMaterial( { color: color } );
                            if (object3D.children[child].geometry) {
                              const piece = new THREE.Mesh( object3D.children[child].geometry, material );
                              piece.rotateX(-1 * Math.PI/2.);
                              piece.rotateZ(Math.PI);
                              scene.add( piece );
                              if ( Math.random() < 0.25 ) piece.layers.enable( BLOOM_SCENE );
                              render();
                            }
                        }
                      }
                    );
                };
                graphicsXRH.send();
            }

            function loadSlicerScene(onLoadFn) {

                const execXHR = new XMLHttpRequest();
                const url = "/slicer/exec";
                execXHR.open('POST', url, true);
                execXHR.responseType = 'json';
                execXHR.onload = onLoadFn;
                execXHR.send(`#python
nodePatterns = ["vtkMRMLSegmentationNode*", "vtkMRMLMarkups*Node*"]
needDemoData = True
for nodePattern in nodePatterns:
  try:
    slicer.util.getNode(nodePattern)
    needDemoData = False
    print(f"Found some {nodePattern}")
  except slicer.util.MRMLNodeNotFoundException:
    pass

if needDemoData:
  filePath = f"{slicer.util.tempDirectory()}/ctacardio-totalseg-models-2023-05-22-Scene.mrb"
  print(f"Downloading to {filePath}")
  demoDataURL = "https://github.com/pieper/SlicerWeb/releases/download/v0.1/ctacardio-totalseg-models-2023-05-22-Scene.mrb"
  slicer.util.downloadFile(demoDataURL, filePath)
  slicer.util.loadScene(filePath)
  import os
  os.remove(filePath)
else:
  print("Showing existing data from scene")
`);
            }

            function setupScene() {

                loadSlicerScene(loadThreeScene);

            }

            function disposeMaterial( obj ) {

                if ( obj.material ) {

                    obj.material.dispose();

                }

            }

            function render() {

                switch ( params.scene ) {

                    case 'Scene only':
                        renderer.render( scene, camera );
                        break;
                    case 'Glow only':
                        renderBloom( false );
                        break;
                    case 'Scene with Glow':
                    default:
                        // render scene with bloom
                        renderBloom( true );

                        // render the entire scene, then render bloom scene on top
                        finalComposer.render();
                        break;

                }

            }

            function renderBloom( mask ) {

                if ( mask === true ) {

                    scene.traverse( darkenNonBloomed );
                    bloomComposer.render();
                    scene.traverse( restoreMaterial );

                } else {

                    camera.layers.set( BLOOM_SCENE );
                    bloomComposer.render();
                    camera.layers.set( ENTIRE_SCENE );

                }

            }

            function darkenNonBloomed( obj ) {

                if ( obj.isMesh && bloomLayer.test( obj.layers ) === false ) {

                    materials[ obj.uuid ] = obj.material;
                    obj.material = darkMaterial;

                }

            }

            function restoreMaterial( obj ) {

                if ( materials[ obj.uuid ] ) {

                    obj.material = materials[ obj.uuid ];
                    delete materials[ obj.uuid ];

                }

            }

        </script>

    </body>

</html>
